﻿using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using Faml.Frontend;
using Faml.LinqCompiler;
using Faml.Optimization;

namespace Faml.Compiler
{
    public sealed class FamlProgram
    {
        private bool makeOptimization = true;
        private TimeSpan timeEvaluation = TimeSpan.FromSeconds(0);

        /// <summary>
        /// Ordered list of expressions to optimize and execute.
        /// </summary>
        private LinkedList<FamlExpressionContainer> expressions;

        private RuntimeEnvironment environment;

        private FamlProgram(RuntimeEnvironment environment, FamlExpression expression)
        {            
            this.environment = environment;
            expressions = new LinkedList<FamlExpressionContainer>();
            expressions.AddFirst(new FamlExpressionContainer(expression));
        }

        /// <summary>
        /// Makes a FamlProgram for a given FamlExpression.
        /// </summary>
        /// <param name="expression">Expanded Faml expression</param>
        /// <param name="environment">Runtime environment</param>
        /// <returns>FamlProgram to optimize and execute</returns>
        public static FamlProgram Make(RuntimeEnvironment environment, FamlExpression expression)
        {
            return new FamlProgram(environment, expression);
        }

        /// <summary>
        /// Compiles and makes a FamlProgram for a given expression in Faml.
        /// </summary>
        /// <param name="expression">Faml expression</param>
        /// <param name="environment">Runtime environment</param>
        /// <returns>FamlProgram to optimize and execute</returns>
        public static FamlProgram Compile(RuntimeEnvironment environment, string expression)
        {            
            Parser parser = new Parser(expression);
            parser.MoveNext();
            environment.Compiler.Reset();
            FamlExpression faml = environment.Compiler.Precompile(parser);
            return Make(environment, faml);
        }

        /// <summary>
        /// Compiles and makes a FamlProgram for a given expression in Faml.
        /// </summary>
        /// <param name="expression">Faml expression</param>
        /// <param name="environment">Runtime environment</param>
        /// <returns>FamlProgram to optimize and execute</returns>
        public static FamlProgram Compile(RuntimeEnvironment environment, string expression, ICollection<FamlDefinition> externalDefs)
        {
            Parser parser = new Parser(expression);
            parser.MoveNext();
            environment.Compiler.Reset();
            foreach (FamlDefinition def in externalDefs)
            {
                environment.Context.AddDefinition(def);
            }
            FamlExpression faml = environment.Compiler.Precompile(parser);
            return Make(environment, faml);
        }

        /// <summary>
        /// Evaluates a given expression in Faml.
        /// </summary>
        /// <param name="expression">Faml expression</param>
        /// <param name="environment">Runtime environment</param>
        /// <returns>FamlProgram to optimize and execute</returns>
        public static object Evaluate(RuntimeEnvironment environment, string expression)
        {
            FamlProgram faml = FamlProgram.Compile(environment, expression);
            return faml.Evaluate();
        }

        /// <summary>
        /// Evaluates a given expression in Faml.
        /// </summary>
        /// <param name="expression">Faml expression</param>
        /// <param name="environment">Runtime environment</param>
        /// <returns>FamlProgram to optimize and execute</returns>
        public static object Evaluate(RuntimeEnvironment environment, string expression, ICollection<FamlDefinition> externalDefs)
        {
            FamlProgram faml = FamlProgram.Compile(environment, expression, externalDefs);
            return faml.Evaluate();
        }

        /// <summary>
        /// Evaluates a given expression in Faml.
        /// </summary>
        /// <param name="expression">Faml expression</param>
        /// <param name="environment">Runtime environment</param>
        /// <returns>FamlProgram to optimize and execute</returns>
        public static object Evaluate(RuntimeEnvironment environment, FamlExpression expression)
        {
            FamlProgram faml = FamlProgram.Make(environment, expression);
            return faml.Evaluate();
        }

        /// <summary>
        /// Gets or sets the value indicating whether optimization is enabled or not.
        /// </summary>
        public bool OptimizationEnabled
        {
            get { return makeOptimization; }
            set { makeOptimization = value; }
        }

        /// <summary>
        /// Gets an inferred type of the FamlProgram's evaluation result.
        /// </summary>
        public Type InferredType
        {
            get 
            {
                if(expressions.Count == 0)
                    throw new Exception("No expression");

                if (expressions.Last.Value.Expression.NodeType == FamlExpressionType.Lambda)
                    return ((FamlLambdaExpression)expressions.Last.Value.Expression).Body.Type;
                return expressions.Last.Value.Expression.Type;
            }
        }

        private string outputExpression = "<not evaluated>";

        public string OutputExpression
        {
            get { return outputExpression; }
        }

        /// <summary>
        /// Evaluates the FamlProgram and returns the result object.
        /// </summary>
        /// <returns>Returns the result of the evaluation.</returns>
        public object Evaluate()
        {
            MakeGlobalOptimization();
            return ExecuteExpressions();
        }

        private object ExecuteExpressions()
        {
            if (expressions.Count == 0)
                throw new ApplicationException("FamlProgram: an expression is missing");

            LinkedListNode<FamlExpressionContainer> nodeContainer = expressions.First;
            object value = null;

            FamlExpressionContainer famlContainer;
            string containerName = "var";
            int index = 1;

            do
            {
                famlContainer = nodeContainer.Value;
                famlContainer.Name = containerName + index.ToString();
                index++;

                MakeLocalOptimization(famlContainer);

                value = ExecuteExpression(famlContainer);
                
                famlContainer.Value = value;                

            } while ((nodeContainer = nodeContainer.Next) != null);

            outputExpression = famlContainer.Expression.ToString();

            return value;
        }

        private void MakeLocalOptimization(FamlExpressionContainer famlContainer)
        {
            FamlExpression faml = famlContainer.Expression;
            if (faml.NodeType == FamlExpressionType.FunctionCall)
            {
                FamlFunctionCallExpression fce = (FamlFunctionCallExpression)faml;
                if (string.Compare(fce.Function, "unfold", true) == 0)
                {
                    int resultRank = fce.Type.GetArrayRank();
                    if (resultRank == 1)
                    {
                        int lenIndex = 0, dataIndex = 1;
                        if (fce.Arguments[1].Type == typeof(int))
                        {
                            lenIndex = 1; dataIndex = 0;
                        }

                        int length = GetValueFrom(fce.Arguments[lenIndex]);

                        FamlLambdaExpression fle = fce.Arguments[dataIndex] as FamlLambdaExpression;
                        FamlLambdaParameter p = fle.Parameters[0];
                        FamlLambdaParameterRange<int> desc = new FamlLambdaParameterRange<int>();
                        desc.Max = length - 1;
                        desc.Min = 0;
                        p.Description = desc;
                    }
                }
            }
        }

        private int GetValueFrom(FamlExpression famlExpression)
        {
            if (famlExpression.NodeType == FamlExpressionType.Constant)
            {
                return (int)((FamlConstant)famlExpression).Value;
            }

            FamlReferenceExpression famlRef = (FamlReferenceExpression)famlExpression;
            return (int)famlRef.ReferencedContainer.Value;
        }

        private object ExecuteExpression(FamlExpressionContainer famlContainer)
        {
            FamlExpression faml = famlContainer.Expression;
            if (SubExpressionAdded != null)
                SubExpressionAdded(this, new SubExpressionAddedEventArgs(famlContainer));

            Expression linq = LinqExpressionCompiler.Compile(faml);
            LambdaExpression lexp;
            if (linq.NodeType == ExpressionType.Lambda)
                lexp = (LambdaExpression)linq;
            else lexp = Expression.Lambda(linq);

            Delegate compiled = lexp.Compile();

            DateTime start = DateTime.Now;
            object result = compiled.DynamicInvoke();
            timeEvaluation += DateTime.Now - start;

            return result;
        }

        private void MakeGlobalOptimization()
        {
            if (!makeOptimization)
                return;

            ExpressionOptimizer optimizer = new ExpressionOptimizer(expressions.First.Value.Expression);
            optimizer.AddVisitors(environment.Visitors);
            expressions = optimizer.Optimize();
        }

        public TimeSpan TimeEvaluation { get { return timeEvaluation; } }


        public event SubExpressionAddedHandler SubExpressionAdded;        
    }

    public delegate void SubExpressionAddedHandler(object sender, SubExpressionAddedEventArgs e);

    public class SubExpressionAddedEventArgs : EventArgs
    {
        private FamlExpressionContainer exp;

        public SubExpressionAddedEventArgs(FamlExpressionContainer exp)
        {
            this.exp = exp;
        }

        public FamlExpressionContainer SubExpression
        {
            get { return exp; }
        }
    }
}
